/*
 * DHCPServer.cpp
 *
 *  Created on: 21.04.2011
 *      Author: mifodix
 */

#include "DHCPServer.hpp"

DHCPServer* DHCPServer::theSingleInstance=NULL;

std::list<MacIPTimes> DHCPServer::boundedAddr;
std::list<MacIPTimes> DHCPServer::offeredAddr;
Settings* DHCPServer::settings=NULL;
volatile uint32_t DHCPServer::nextDynIP=0;

SocketUDP DHCPServer::broadcastListener;
SocketUDP DHCPServer::broadcastSender;

boost::shared_mutex DHCPServer::offeredMutex;
boost::shared_mutex DHCPServer::boundedMutex;
boost::mutex DHCPServer::broadcastListenerMutex;
boost::mutex DHCPServer::broadcastSenderMutex;
boost::mutex DHCPServer::nextDynIPMutex;

DHCPServer::DHCPServer()
{
}

DHCPServer* DHCPServer::GetInstance()
{
	if (!theSingleInstance)
	{
		theSingleInstance=new DHCPServer;
	}
	return theSingleInstance;
}

PacketEditor* DHCPServer::AddOptionsFromSettings(PacketEditor *p)
{
	PacketEditor *replyPacket=new PacketEditor(*p);

	char *opData=NULL;
	unsigned char opLen;
	uint32_t temp4Bytes=NetUtils::HostOrd2NetOrd(settings->GetNetmask());
	replyPacket->AddOption(OPTION_SUBNET_MASK, sizeof(temp4Bytes), (char*) &temp4Bytes);

	temp4Bytes=NetUtils::HostOrd2NetOrd(settings->GetServer());
	replyPacket->AddOption(OPTION_SERVER_IDENTIFIER, sizeof(temp4Bytes), (char*) &temp4Bytes);

	std::list<uint32_t> tempList=settings->GetRouters();
	if (!tempList.empty())
	{
		opLen=NetUtils::List2Chars(tempList, opData, true);
		replyPacket->AddOption(OPTION_ROUTER, opLen, opData);
		delete[] opData;
		opData=NULL;
	}
	tempList=settings->GetDomainNameServers();
	if (!tempList.empty())
	{
		opLen=NetUtils::List2Chars(tempList, opData, true);
		replyPacket->AddOption(OPTION_DOMAIN_NAME_SERVER, opLen, opData);
		delete[] opData;
		opData=NULL;
	}
	return replyPacket;
}

bool DHCPServer::IsIPAvailable(uint32_t ip)
{
	if (settings->IsInRange(ip) && !settings->FindReservedIP(ip) && !IsIPAllocated(boundedAddr, ip) && !IsIPAllocated(offeredAddr, ip))
	{
		return true;
	}
	return false;
}

uint32_t DHCPServer::FindIP(std::list<MacIPTimes> list, const char *mac)
{
	boundedMutex.lock_shared();
	offeredMutex.lock_shared();
	for (std::list<MacIPTimes>::iterator it=list.begin(); it!=list.end(); ++it)
	{
		if (*it==mac)
		{
			boundedMutex.unlock_shared();
			offeredMutex.unlock_shared();
			return it->GetIP();
		}
	}
	boundedMutex.unlock_shared();
	offeredMutex.unlock_shared();
	return 0;
}

uint32_t DHCPServer::FindLease(std::list<MacIPTimes> list, const char *mac)
{
	boundedMutex.lock_shared();
	offeredMutex.lock_shared();
	for (std::list<MacIPTimes>::iterator it=list.begin(); it!=list.end(); ++it)
	{
		if (*it==mac)
		{
			boundedMutex.unlock_shared();
			offeredMutex.unlock_shared();
			return it->GetLeaseTime();
		}
	}
	boundedMutex.unlock_shared();
	offeredMutex.unlock_shared();
	return 0;
}

bool DHCPServer::IsIPAllocated(std::list<MacIPTimes> list, uint32_t ip)
{
	boundedMutex.lock_shared();
	offeredMutex.lock_shared();
	for (std::list<MacIPTimes>::iterator it=list.begin(); it!=list.end(); ++it)
	{
		if (*it==ip)
		{
			boundedMutex.unlock_shared();
			offeredMutex.unlock_shared();
			return true;
		}
	}
	boundedMutex.unlock_shared();
	offeredMutex.unlock_shared();
	return false;
}

void DHCPServer::FreeOldOffers()
{
	offeredMutex.lock_upgrade();
	offeredMutex.unlock_upgrade_and_lock();
	for (std::list<MacIPTimes>::iterator it=offeredAddr.begin(); it!=offeredAddr.end(); ++it)
	{
		if (time(NULL)-it->GetCreationTime()>OFFER_EXPIRATION_TIME)
		{
			offeredAddr.remove(*it);
		}
	}
	offeredMutex.unlock_and_lock_upgrade();
	offeredMutex.unlock_upgrade();
}

void DHCPServer::FreeExpiredBoundings()
{
	boundedMutex.lock_upgrade();
	boundedMutex.unlock_upgrade_and_lock();
	for (std::list<MacIPTimes>::iterator it=boundedAddr.begin(); it!=boundedAddr.end(); ++it)
	{
		if (time(NULL)-it->GetCreationTime()>it->GetLeaseTime())
		{
			boundedAddr.remove(*it);
		}
	}
	boundedMutex.unlock_and_lock_upgrade();
	boundedMutex.unlock_upgrade();
}

bool DHCPServer::ExtendBounding(const char *mac)
{
	boundedMutex.lock_upgrade();
	boundedMutex.unlock_upgrade_and_lock();
	for (std::list<MacIPTimes>::iterator it=boundedAddr.begin(); it!=boundedAddr.end(); ++it)
	{
		if (*it==mac)
		{
			it->SetCreationTime(time(NULL));//устанавливаем время связывания = текущему, чтобы не удалилось в FreeExpiredBoundings()
			boundedMutex.unlock_and_lock_upgrade();
			boundedMutex.unlock_upgrade();
			return true;
		}
	}
	boundedMutex.unlock_and_lock_upgrade();
	boundedMutex.unlock_upgrade();
	return false;
}

bool DHCPServer::OfferIPnLease(const char *mac, uint32_t &ip, uint32_t &lease)
{
	//удалим устаревшие предложения, на которые не был дан ответ
	FreeOldOffers();
	if (!ip)//выделяем новый ip из диапазона
	{
		if (settings->GetRangeBegin()==0 || settings->GetRangeEnd()==0)//если не задан диапазон дин адресов
		{
			return false;
		}
		nextDynIPMutex.lock();
		if (!nextDynIP || nextDynIP>settings->GetRangeEnd())//если последний дин ip==0, то мы ещё не выделяли из диапазона
		{													//если дин ip > потолка диапазона, то пытаемся найти высвободившиеся адреса с начала диапазона
			nextDynIP=settings->GetRangeBegin();//инициализируем счётчик дин адресов
		}
		uint32_t tempIP=nextDynIP;
		while (!IsIPAvailable(tempIP))//подбираем незарезервированный IP
		{
			if (++tempIP>settings->GetRangeEnd())
			{
				return false;
			}
		}
		ip=tempIP;
		nextDynIP=tempIP+1;//устанавливаем след дин ip
		nextDynIPMutex.unlock();
	}
	if (!lease)//устанавливаем время по умолчанию
	{
		lease=settings->GetDefaultLeaseTime();
	}
	//сохраняем предложенную конфигурацию
	MacIPTimes offer(mac, MAC_SIZE, ip, lease);
	offeredMutex.lock_upgrade();
	offeredMutex.unlock_upgrade_and_lock();

	offeredAddr.push_back(offer);

	offeredMutex.unlock_and_lock_upgrade();
	offeredMutex.unlock_upgrade();

	return true;
}

bool DHCPServer::BindIPnLease(const char *mac, uint32_t &ip, uint32_t &lease)
{
	FreeExpiredBoundings();//удаляем записи с истекшим временем аренды
	boundedMutex.lock_upgrade();
	boundedMutex.unlock_upgrade_and_lock();

	offeredMutex.lock_upgrade();
	offeredMutex.unlock_upgrade_and_lock();

	for (std::list<MacIPTimes>::iterator it=offeredAddr.begin(); it!=offeredAddr.end(); ++it)
	{
		if (*it==mac)
		{
			boundedAddr.push_back(*it);//добавим в привязанные адреса
			ip=it->GetIP();
			lease=it->GetLeaseTime();
			offeredAddr.remove(*it);//удалим из предложений

			boundedMutex.unlock_and_lock_upgrade();
			boundedMutex.unlock_upgrade();

			offeredMutex.unlock_and_lock_upgrade();
			offeredMutex.unlock_upgrade();
			return true;
		}
	}
	boundedMutex.unlock_and_lock_upgrade();
	boundedMutex.unlock_upgrade();

	offeredMutex.unlock_and_lock_upgrade();
	offeredMutex.unlock_upgrade();
	return false;
}

PacketEditor* DHCPServer::ProcessDiscover(PacketEditor p)
{
	char *clientMac=p.GetChaddr();//получаем mac клиента из сообщения
	//пытаемся угодить клиенту при выборе IP
	uint32_t IP=0;
	Option *op=NULL;
	char *opData=NULL;
	unsigned char opLen;
	if ((IP=FindIP(boundedAddr, clientMac))==0)//если этому клиенту ещё не выделен ip
	{
		if ((IP=FindIP(offeredAddr, clientMac))==0)//если этому клиенту ещё не предлагался ip
		{
			if ((IP=settings->FindReservedIP(clientMac))==0)//если клиенту не прописан постоянный ip, то пытаемся использовать запрашиваемый
			{
				if ((op=p.FindOption(OPTION_REQUEST_IP))!=NULL)//если у клиента есть опция с запрашиваемым IP
				{
					opLen=op->GetLen();
					opData=op->GetData();
					uint32_t requestedIP;
					memmove(&requestedIP, opData, opLen);//получаем ip в сетевом порядке
					requestedIP=NetUtils::NetOrd2HostOrd(requestedIP);//переводим ip в порядок хоста
					if (IsIPAvailable(requestedIP))//если ip доступен, то привязываем его к клиенту
					{
						IP=requestedIP;
					}
				}
			}
		}
	}
	//не забываем освобождать память
	if (!op)
	{
		delete op;
		op=NULL;
	}
	if (!opData)
	{
		delete[] opData;
		opData=NULL;
	}
	//пытаемся угодить клиенту в выборе времени аренды
	uint32_t lease=0;
	uint32_t requestedLease;
	if ((op=p.FindOption(OPTION_REQUEST_TIME))!=NULL)//если у клиента есть опция запроса времени аренды
	{
		opLen=op->GetLen();
		opData=op->GetData();
		memmove(&requestedLease, opData, opLen);
		requestedLease=NetUtils::NetOrd2HostOrd(requestedLease);
		if (requestedLease<=settings->GetMaxLeaseTime() && requestedLease>0)//если запрашиваемое время нас устраивает
		{
			lease=requestedLease;
		}
	}
	else//пытаемся найти предыдущее время аренды, выданное этому клиенту
	{
		lease=FindLease(boundedAddr, clientMac);
	}
	char errorMess[32];//здесь будет храниться передаваемая клиенту ошибка
	if (!OfferIPnLease(clientMac, IP, lease))//предлогаем новый IP и время аренды
	{
		Logger::Log("DHCP server", STATE_ERROR, "No more free IP adresses!");
		return NULL;//если невозможно предложить адрес
	}
	//формируем пакет DHCPOFFER
	PacketEditor *replyPacket=new PacketEditor(p);
	replyPacket->ClearOptions();//удаляем все опции от старого пакета
	replyPacket->SetOp(BOOT_REPLY);//тип сообщения - ответ
	replyPacket->SetHops(0);//кол-во промежуточных серверов 0
	replyPacket->SetSecs(0);
	replyPacket->SetCiaddr(0);
	replyPacket->SetYiaddr(IP);//предлогаем IP
	replyPacket->SetSiaddr(settings->GetServer());//возвращаем адрес DHCP-сервера
	replyPacket->SetSname("tinyDCHP server");//записываем имя сервера

	char tempChar=DHCPOFFER;
	replyPacket->AddOption(OPTION_MESSAGE_TYPE, sizeof(tempChar), &tempChar);//опция тип сообщения - предложение

	uint32_t temp4Bytes=NetUtils::HostOrd2NetOrd(lease);
	replyPacket->AddOption(OPTION_REQUEST_TIME, sizeof(lease), (char *) &temp4Bytes);

	replyPacket=AddOptionsFromSettings(replyPacket);

	if (!clientMac) delete[] clientMac;
	if (!op) delete op;
	if (!opData) delete[] opData;

	return replyPacket;
}

PacketEditor* DHCPServer::ProcessRequest(PacketEditor p)
{
	Option *op=NULL;
	char *opData=NULL;
	unsigned char opLen;
	uint32_t requestedIP;
	uint32_t IP;
	uint32_t lease;
	PacketEditor *replyPacket=NULL;
	if ((op=p.FindOption(OPTION_SERVER_IDENTIFIER))!=NULL)//состояние клиента SELECTING
	{
		BindIPnLease(p.GetChaddr(), IP, lease);//осуществляем новую привязку mac клиента к ip
		//отправить DHCPAK
	}
	else if (p.GetCiaddr()==0)//состояние клиента INIT-REBOOT
	{
		op=p.FindOption(OPTION_REQUEST_IP);
		opLen=op->GetLen();
		opData=op->GetData();
		memmove(&requestedIP, opData, opLen);
		requestedIP=NetUtils::NetOrd2HostOrd(requestedIP);
		//проверка корректности IP и его наличия среди привязанных IP
		if (NetUtils::CheckIPSubnet(requestedIP, settings->GetSubnet(), settings->GetNetmask()) && requestedIP==FindIP(boundedAddr, p.GetChaddr()))
		{
			//отправить DHCPAK
			IP=requestedIP;
			lease=FindLease(boundedAddr, p.GetChaddr());
			ExtendBounding(p.GetChaddr());
		}
		else
		{
			//отправить DHCPNAK
			replyPacket=new PacketEditor(p);
			replyPacket->SetOp(BOOT_REPLY);
			replyPacket->SetHops(0);
			replyPacket->SetSecs(0);
			replyPacket->SetCiaddr(0);
			replyPacket->SetYiaddr(0);
			replyPacket->SetSiaddr(0);

			replyPacket->ClearOptions();//удаляем старые опции
			char tempChar=DHCPNAK;
			replyPacket->AddOption(OPTION_MESSAGE_TYPE, sizeof(tempChar), &tempChar);

			uint32_t temp4Bytes=NetUtils::HostOrd2NetOrd(settings->GetServer());
			replyPacket->AddOption(OPTION_SERVER_IDENTIFIER, sizeof(temp4Bytes), (char*) &temp4Bytes);

			return replyPacket;
		}
	}
	else //состояние клиента RENEWING (пришло unicast)
	{
		//отправить DHCPAK с IP из ciaddr и увеличить время аренды
		IP=p.GetCiaddr();
		lease=FindLease(boundedAddr, p.GetChaddr());
		ExtendBounding(p.GetChaddr());//увеличиваем время аренды
	}
	replyPacket=new PacketEditor(p);//формируем DHCPAK
	replyPacket->SetOp(BOOT_REPLY);
	replyPacket->SetHops(0);
	replyPacket->SetSecs(0);
	replyPacket->SetYiaddr(IP);
	replyPacket->SetSiaddr(settings->GetServer());
	replyPacket->SetSname("tinyDCHP server");//записываем имя сервера
	replyPacket->ClearOptions();//удаляем старые опции
	char tempChar=DHCPACK;
	replyPacket->AddOption(OPTION_MESSAGE_TYPE, sizeof(tempChar), &tempChar);

	uint32_t temp4Bytes=NetUtils::HostOrd2NetOrd(lease);
	replyPacket->AddOption(OPTION_REQUEST_TIME, sizeof(lease), (char *) &temp4Bytes);

	replyPacket=AddOptionsFromSettings(replyPacket);

	if (!op) delete op;
	if (!opData) delete[] opData;
	return replyPacket;
}

void DHCPServer::ProcessDecline(PacketEditor p)
{
	Option *op=p.FindOption(OPTION_REQUEST_IP);//ищем опцию с типом сообщения
	unsigned char opLen=op->GetLen();
	char *opData=op->GetData();
	uint32_t wrongIP;
	memmove(&wrongIP, opData, opLen);
	char *strIP=NetUtils::IP2String(wrongIP);
	strcat(strIP, " declined");
	Logger::Log("DHCP server", STATE_WARNING, strIP);
}

void DHCPServer::ProcessRelease(PacketEditor p)
{
	uint32_t releasedIP=p.GetCiaddr();
	boundedMutex.lock_upgrade();
	boundedMutex.unlock_upgrade_and_lock();
	for (std::list<MacIPTimes>::iterator it=boundedAddr.begin(); it!=boundedAddr.end(); ++it)
	{
		if (*it==releasedIP)
		{
			boundedAddr.remove(*it);
			boundedMutex.unlock_and_lock_upgrade();
			boundedMutex.unlock_upgrade();
			return;
		}
	}
	boundedMutex.unlock_and_lock_upgrade();
	boundedMutex.unlock_upgrade();
}

PacketEditor* DHCPServer::ProcessInform(PacketEditor p)
{
	PacketEditor *replyPacket=new PacketEditor(p);//формируем DHCPAK
	replyPacket->SetOp(BOOT_REPLY);
	replyPacket->SetHops(0);
	replyPacket->SetSecs(0);

	replyPacket->SetSiaddr(settings->GetServer());
	replyPacket->SetSname("tinyDCHP server");//записываем имя сервера
	replyPacket->ClearOptions();//удаляем старые опции
	char tempChar=DHCPACK;
	replyPacket->AddOption(OPTION_MESSAGE_TYPE, sizeof(tempChar), &tempChar);

	replyPacket=AddOptionsFromSettings(replyPacket);

	return replyPacket;
}

void DHCPServer::thread_func(PacketEditor p)
{
	Option *op=p.FindOption(53);//ищем опцию с типом сообщения
	if (op)
	{
		char *messType=op->GetData();//узнаём тип сообщения
		PacketEditor *replyPacket=NULL;
		char *packet=NULL;
		size_t packetSize;

		switch(*messType)
		{
		case DHCPDISCOVER:
			replyPacket=ProcessDiscover(p);
			break;
		case DHCPREQUEST:
			replyPacket=ProcessRequest(p);
			break;
		case DHCPDECLINE://обработка сообщения о уже используемом  IP
			ProcessDecline(p);
			return;
			break;
		case DHCPRELEASE:
			ProcessRelease(p);
			return;
			break;
		case DHCPINFORM:
			replyPacket=ProcessInform(p);
			break;
		default:
			Logger::Log("DHCP server", STATE_WARNING, "Invalid type of incoming message");
			break;
		}
		if (replyPacket)
		{
			packetSize=replyPacket->GetPacket(packet);
			broadcastSenderMutex.lock();
			broadcastSender.SendAll(packet, packetSize, "255.255.255.255", CLIENT_PORT);//отправляем пакет
			broadcastSenderMutex.unlock();
			delete replyPacket;
		}
		if (!messType) delete[] messType;
		if (!packet) delete[] packet;
	}
	else
	{
		Logger::Log("DHCP server", STATE_WARNING, "Can't define type of incoming message");
	}
}

void DHCPServer::start(const char *cfgFilename)
{
	Logger::Log("DHCP server", STATE_INFO, "Server was started.");
	ConfigParser cfgParser;
	settings=cfgParser.parse(cfgFilename);
	if (!settings)
	{
		Logger::Log("DHCP server", STATE_ERROR, "Configuration of server failed!");
		return;
	}
	Logger::Log("DHCP server", STATE_INFO, "Configuration of server successfully completed.");
	if (!broadcastListener.Bind("0.0.0.0", SERVER_PORT, true))
	{
		Logger::Log("DHCP server", STATE_ERROR, "Can't create broadcast listening socket!");
		return;
	}
	Logger::Log("DHCP server", STATE_INFO, "Created broadcast listening socket.");

	if (!broadcastSender.Bind(settings->GetServer(), 68, true))
	{
		Logger::Log("DHCP server", STATE_ERROR, "Can't create broadcast sending socket!");
		return;
	}
	Logger::Log("DHCP server", STATE_INFO, "Created broadcast sending socket.");

	size_t messSize=MAX_PACKET_SIZE;
	char *mess=new char[messSize];//пытаемся принять пакет максимального размера
	char *fromAddress=new char[ADDR_STR_SIZE];

	uint16_t port;
	boost::thread_group threadGroup;//группа потоков-обработчиков
	while (true)//цикл обработки пакетов-сообщений
	{
		broadcastListener.Receive(mess, messSize, fromAddress, port);//приняли пакет
		PacketEditor pe(mess, messSize);//инициализация пакета
		threadGroup.add_thread(new boost::thread(thread_func, pe));
	}
}

DHCPServer::~DHCPServer()
{
	if (settings)
	{
		delete settings;
	}
	Logger::Log("DHCP server", STATE_INFO, "Server was stopped.");
}
